{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Interfaces and Abstract Base Classes\n",
    "\n",
    "This notebook contains example code from [*Fluent Python*](http://shop.oreilly.com/product/0636920032519.do), by Luciano Ramalho.\n",
    "\n",
    "Code by Luciano Ramalho, modified by Allen Downey.\n",
    "\n",
    "MIT License: https://opensource.org/licenses/MIT"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Let's first toggle on doctest mode**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Exception reporting mode: Plain\n",
      "Doctest mode is: ON\n"
     ]
    }
   ],
   "source": [
    "%doctest_mode"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h1> Monkey Patching</h1>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the FrenchDeck example from chapter 1, the 'FrenchDeck' cannot be shuffled because the object does not support item assignment. The problem is that shuffle operates by swapping items inside the collection, and FrenchDeck only implements the immutable sequence protocol. Mutable sequences must also provide a __setitem__ method.\n",
    "\n",
    "Because Python is dynamic, we can fix this at runtime."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "First, create the FrenchDeck class"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "import collections\n",
    "Card = collections.namedtuple('Card', ['rank', 'suit'])\n",
    "\n",
    "class FrenchDeck:\n",
    "    ranks = [str(n) for n in range(2, 11)] + list('JQKA') \n",
    "    suits = 'spades diamonds clubs hearts'.split()\n",
    "    \n",
    "    def __init__(self):\n",
    "        self._cards = [Card(rank, suit) for suit in self.suits\n",
    "                       for rank in self.ranks]\n",
    "   \n",
    "    def __len__(self):\n",
    "        return len(self._cards) \n",
    "    \n",
    "    def __getitem__(self, position): \n",
    "        return self._cards[position]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As explained above, attempting to shuffle the deck will produce a TypeError"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "'FrenchDeck' object does not support item assignment",
     "output_type": "error",
     "traceback": [
      "Traceback \u001b[0;36m(most recent call last)\u001b[0m:\n",
      "  File \u001b[1;32m\"<ipython-input-6-910415cc5ddb>\"\u001b[0m, line \u001b[1;32m3\u001b[0m, in \u001b[1;35m<module>\u001b[0m\n    shuffle(deck)\n",
      "\u001b[0;36m  File \u001b[0;32m\"//anaconda/lib/python3.5/random.py\"\u001b[0;36m, line \u001b[0;32m272\u001b[0;36m, in \u001b[0;35mshuffle\u001b[0;36m\u001b[0m\n\u001b[0;31m    x[i], x[j] = x[j], x[i]\u001b[0m\n",
      "\u001b[0;31mTypeError\u001b[0m\u001b[0;31m:\u001b[0m 'FrenchDeck' object does not support item assignment\n"
     ]
    }
   ],
   "source": [
    ">>> from random import shuffle\n",
    ">>> deck = FrenchDeck()\n",
    ">>> shuffle(deck)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h3>Solution: Monkey patch it</h3>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[Card(rank='3', suit='hearts'), Card(rank='4', suit='diamonds'), Card(rank='4', suit='clubs'), Card(rank='7', suit='hearts'), Card(rank='9', suit='spades')]"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    ">>> def set_card(deck, position, card):\n",
    "        deck._cards[position] = card\n",
    ">>> FrenchDeck.__setitem__ = set_card\n",
    ">>> shuffle(deck)\n",
    ">>> deck[:5]\n",
    "[Card(rank='3', suit='hearts'), Card(rank='4', suit='diamonds'), Card(rank='4', suit='clubs'), Card(rank='7', suit='hearts'), Card(rank='9', suit='spades')]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h3>Or we could subclass the collections.MutableSequence ABC</h3>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "class FrenchDeck2(collections.MutableSequence):\n",
    "    ranks = [str(n) for n in range(2, 11)] + list('JQKA')\n",
    "    suits = 'spades diamonds clubs hearts'.split()\n",
    "\n",
    "    def __init__(self):\n",
    "        self._cards = [Card(rank, suit) for suit in self.suits\n",
    "                                        for rank in self.ranks]\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self._cards)\n",
    "\n",
    "    def __getitem__(self, position):\n",
    "        return self._cards[position]\n",
    "\n",
    "    def __setitem__(self, position, value):\n",
    "        self._cards[position] = value\n",
    "\n",
    "    def __delitem__(self, position):\n",
    "        del self._cards[position]\n",
    "\n",
    "    def insert(self, position, value):\n",
    "        self._cards.insert(position, value)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h1>Creating an abstract base class (ABC)</h1>\n",
    "\n",
    "This example creates an ABC to support user-provided nonrepeating random-picking classes.\n",
    "\n",
    "It is named [*Tombola*](http://www.oxforddictionaries.com/us/definition/american_english/tombola), after the Italian name of bingo and the tumbling container that mixes the numbers. \n",
    "\n",
    "This ABC will have \n",
    "* two abstract methods -- .load() and .pick()\n",
    "    \n",
    "    * We can’t know how concrete subclasses will store the items, but we can build the inspect result by emptying the Tombola with successive calls to .pick() and then use .load() to put everything back.\n",
    "    \n",
    "* two concrete methods -- .loaded() and .inspect(). \n",
    "     \n",
    "     * It's OK to provide concrete methods in ABCs, as long as they only depend on other methods in the interface"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import abc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "class Tombola(abc.ABC):   #subclass abc.ABC to define an ABC in Python 3.4+\n",
    " \n",
    "    @abc.abstractmethod   #An abstract method is marked with the @abstractmethod decorator. \n",
    "                          #Often its body is empty except for a docstring.\n",
    "    def load(self, iterable):     \n",
    "        \"\"\"Add items from an iterable.\"\"\"\n",
    " \n",
    "    @abc.abstractmethod\n",
    "    def pick(self):\n",
    "        \"\"\"Remove item at random, returning it. This method should raise `LookupError` when the instance is empty.\"\"\"\n",
    "    \n",
    "    def loaded(self):\n",
    "        \"\"\"Return `True` if there's at least 1 item, `False` otherwise.\"\"\" \n",
    "        return bool(self.inspect())\n",
    "\n",
    "    def inspect(self): \n",
    "        \"\"\"Return a sorted tuple with the items currently inside.\"\"\" \n",
    "        items = [] \n",
    "        while True:\n",
    "            try:\n",
    "                items.append(self.pick())\n",
    "            except LookupError:\n",
    "                break\n",
    "        self.load(items)\n",
    "        return tuple(sorted(items))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To witness the interface checking per‐ formed by an ABC, let’s try to fool Tombola with a defective implementation:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from tombola import Tombola\n",
    "class Fake(Tombola):\n",
    "    def pick(self): \n",
    "        return 13"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 73,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "__main__.Fake"
      ]
     },
     "execution_count": 73,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Fake"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "Can't instantiate abstract class Fake with abstract methods load",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-22-eb09ed8f651b>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mf\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mFake\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m: Can't instantiate abstract class Fake with abstract methods load"
     ]
    }
   ],
   "source": [
    "f = Fake()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We should see \"TypeError: Can't instantiate abstract class Fake with abstract methods load\"\n",
    "\n",
    "Fake is considered abstract because it failed to implement load, one of the abstract methods declared in the Tombola ABC"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h1>Given the Tombola ABC, we’ll now develop two concrete subclasses that satisfy its interface: BingoCage, and LotteryBlower</h1>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h2>bingo.py: BingoCage is a concrete subclass of Tombola</h2>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import random\n",
    "from tombola import Tombola"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "class BingoCage(Tombola):\n",
    "    def __init__(self, items):\n",
    "        self._randomizer = random.SystemRandom() \n",
    "        self._items = []\n",
    "        self.load(items)\n",
    "        \n",
    "    def load(self, items): \n",
    "        self._items.extend(items)\n",
    "        self._randomizer.shuffle(self._items)\n",
    "        \n",
    "    def pick(self):\n",
    "        try:\n",
    "            return self._items.pop()\n",
    "        except IndexError:\n",
    "            raise LookupError('pick from empty BingoCage')\n",
    "    \n",
    "    def __call__(self):\n",
    "        self.pick()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "we can be lazy and just inherit the suboptimal concrete methods from the Tombola ABC, ie. inspect() and loaded()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h2>lotto.py: LotteryBlower is a concrete subclass that overrides the inspect and loaded methods from Tombola</h2>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import random\n",
    "from tombola import Tombola"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class LotteryBlower(Tombola): \n",
    "    \n",
    "    def __init__(self, iterable):\n",
    "        self._balls = list(iterable)   #flexible because the iterable argument may be any iterable type \n",
    "                                       #but are stored in as list\n",
    "        \n",
    "    def load(self, iterable):\n",
    "        self._balls.extend(iterable)\n",
    "\n",
    "    def pick(self):\n",
    "        try:\n",
    "            position = random.randrange(len(self._balls))\n",
    "        except ValueError:\n",
    "            raise LookupError('pick from empty BingoCage') #catch ValueError and throw LookupError instead \n",
    "                                                           #to be compatible with Tombola\n",
    "        return self._balls.pop(position)\n",
    "\n",
    "    def loaded(self):    #Override loaded to avoid calling inspect\n",
    "        return bool(self._balls)\n",
    "\n",
    "    def inspect(self):   #Override inspect with one-liner.\n",
    "        return tuple(sorted(self._balls))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h1>Goose Typing</h1>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By calling a register method on the ABC, the registered class becomes a virtual subclass of the ABC, \n",
    "and will be recognized as such by functions like issubclass and isinstance, \n",
    "but it will not inherit any methods or attributes from the ABC"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h3>tombolist.py: class TomboList is a virtual subclass of Tombola</h3>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from random import randrange\n",
    "from tombola import Tombola"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "@Tombola.register \n",
    "class TomboList(list):\n",
    "    def pick(self): \n",
    "        if self:\n",
    "            position = randrange(len(self))\n",
    "            return self.pop(position)\n",
    "        else:\n",
    "            raise LookupError('pop from empty TomboList')\n",
    "        load = list.extend\n",
    "\n",
    "    def loaded(self):\n",
    "        return bool(self)\n",
    "    \n",
    "    def inspect(self):\n",
    "        return tuple(sorted(self))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "__main__.TomboList"
      ]
     },
     "execution_count": 38,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Tombola.register(TomboList)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note:  If you’re using Python 3.3 or earlier, you can’t use .register as a class decorator. You must use standard call syntax."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h3>Test the virtual subclass</h3>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 65,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 65,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    ">>> from tombola import Tombola \n",
    ">>> from tombolist import TomboList\n",
    ">>> issubclass(TomboList, Tombola) \n",
    "True\n",
    ">>> t = TomboList(range(100))\n",
    ">>> isinstance(t, Tombola)\n",
    "True"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h3>Check the MRO (Method Resolution Order)</h3>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 67,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(tombolist.TomboList, list, object)"
      ]
     },
     "execution_count": 67,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "TomboList.__mro__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tombola is not in Tombolist.__mro__, so Tombolist does not inherit any methods from Tombola"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h2>Run tombola_runner.py to test all Tombola subclasses</h2>\n",
    "\n",
    "First, reset the interpreter to remove all user-defined names from the namespace"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 94,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Once deleted, variables cannot be recovered. Proceed (y/[n])? y\n"
     ]
    }
   ],
   "source": [
    "%reset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 95,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TumblingDrum     24 tests,  0 failed - OK\n",
      "LotteryBlower    24 tests,  0 failed - OK\n",
      "BingoCage        24 tests,  0 failed - OK\n",
      "TomboList        24 tests,  0 failed - OK\n"
     ]
    }
   ],
   "source": [
    "%run tombola_runner.py"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h2>A class can be recognized as a virtual subclass of an ABC even without registration if the ABC implements a special class method named __subclasshook__ -- but this should rarely be used</h2>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 98,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 98,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    ">>> class Struggle:\n",
    "        def __len__(self):\n",
    "            return 23\n",
    "        \n",
    ">>> from collections import abc\n",
    ">>> isinstance(Struggle(), abc.Sized) \n",
    "True\n",
    ">>> issubclass(Struggle, abc.Sized) \n",
    "True"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
